down_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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/docker/docker/api/types/container"
  23. "github.com/docker/docker/api/types/filters"
  24. "github.com/docker/docker/api/types/image"
  25. "github.com/docker/docker/api/types/network"
  26. "github.com/docker/docker/api/types/volume"
  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. []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. volume.ListOptions{
  48. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  49. }).
  50. Return(volume.ListResponse{}, 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(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  54. Return([]network.Summary{
  55. {ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  56. {ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  57. }, nil)
  58. stopOptions := container.StopOptions{}
  59. api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
  60. api.EXPECT().ContainerStop(gomock.Any(), "456", stopOptions).Return(nil)
  61. api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(nil)
  62. api.EXPECT().ContainerRemove(gomock.Any(), "123", container.RemoveOptions{Force: true}).Return(nil)
  63. api.EXPECT().ContainerRemove(gomock.Any(), "456", container.RemoveOptions{Force: true}).Return(nil)
  64. api.EXPECT().ContainerRemove(gomock.Any(), "789", container.RemoveOptions{Force: true}).Return(nil)
  65. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{
  66. Filters: filters.NewArgs(
  67. projectFilter(strings.ToLower(testProject)),
  68. networkFilter("default")),
  69. }).Return([]network.Summary{
  70. {ID: "abc123", Name: "myProject_default"},
  71. {ID: "def456", Name: "myProject_default"},
  72. }, nil)
  73. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
  74. api.EXPECT().NetworkInspect(gomock.Any(), "def456", gomock.Any()).Return(network.Inspect{ID: "def456"}, nil)
  75. api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
  76. api.EXPECT().NetworkRemove(gomock.Any(), "def456").Return(nil)
  77. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{})
  78. assert.NilError(t, err)
  79. }
  80. func TestDownWithGivenServices(t *testing.T) {
  81. mockCtrl := gomock.NewController(t)
  82. defer mockCtrl.Finish()
  83. api, cli := prepareMocks(mockCtrl)
  84. tested, err := NewComposeService(cli)
  85. assert.NilError(t, err)
  86. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  87. []container.Summary{
  88. testContainer("service1", "123", false),
  89. testContainer("service2", "456", false),
  90. testContainer("service2", "789", false),
  91. testContainer("service_orphan", "321", true),
  92. }, nil)
  93. api.EXPECT().VolumeList(
  94. gomock.Any(),
  95. volume.ListOptions{
  96. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  97. }).
  98. Return(volume.ListResponse{}, nil)
  99. // network names are not guaranteed to be unique, ensure Compose handles
  100. // cleanup properly if duplicates are inadvertently created
  101. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  102. Return([]network.Summary{
  103. {ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  104. {ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  105. }, nil)
  106. stopOptions := container.StopOptions{}
  107. api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
  108. api.EXPECT().ContainerRemove(gomock.Any(), "123", container.RemoveOptions{Force: true}).Return(nil)
  109. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{
  110. Filters: filters.NewArgs(
  111. projectFilter(strings.ToLower(testProject)),
  112. networkFilter("default")),
  113. }).Return([]network.Summary{
  114. {ID: "abc123", Name: "myProject_default"},
  115. }, nil)
  116. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
  117. api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(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(
  130. []container.Summary{
  131. testContainer("service1", "123", false),
  132. testContainer("service2", "456", false),
  133. testContainer("service2", "789", false),
  134. testContainer("service_orphan", "321", true),
  135. }, nil)
  136. api.EXPECT().VolumeList(
  137. gomock.Any(),
  138. volume.ListOptions{
  139. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  140. }).
  141. Return(volume.ListResponse{}, nil)
  142. // network names are not guaranteed to be unique, ensure Compose handles
  143. // cleanup properly if duplicates are inadvertently created
  144. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  145. Return([]network.Summary{
  146. {ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  147. {ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
  148. }, nil)
  149. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{
  150. Services: []string{"not-running-service1", "not-running-service2"},
  151. })
  152. assert.NilError(t, err)
  153. }
  154. func TestDownRemoveOrphans(t *testing.T) {
  155. mockCtrl := gomock.NewController(t)
  156. defer mockCtrl.Finish()
  157. api, cli := prepareMocks(mockCtrl)
  158. tested, err := NewComposeService(cli)
  159. assert.NilError(t, err)
  160. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return(
  161. []container.Summary{
  162. testContainer("service1", "123", false),
  163. testContainer("service2", "789", false),
  164. testContainer("service_orphan", "321", true),
  165. }, nil)
  166. api.EXPECT().VolumeList(
  167. gomock.Any(),
  168. volume.ListOptions{
  169. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  170. }).
  171. Return(volume.ListResponse{}, nil)
  172. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  173. Return([]network.Summary{
  174. {
  175. Name: "myProject_default",
  176. Labels: map[string]string{compose.NetworkLabel: "default"},
  177. },
  178. }, nil)
  179. stopOptions := container.StopOptions{}
  180. api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
  181. api.EXPECT().ContainerStop(gomock.Any(), "789", stopOptions).Return(nil)
  182. api.EXPECT().ContainerStop(gomock.Any(), "321", stopOptions).Return(nil)
  183. api.EXPECT().ContainerRemove(gomock.Any(), "123", container.RemoveOptions{Force: true}).Return(nil)
  184. api.EXPECT().ContainerRemove(gomock.Any(), "789", container.RemoveOptions{Force: true}).Return(nil)
  185. api.EXPECT().ContainerRemove(gomock.Any(), "321", container.RemoveOptions{Force: true}).Return(nil)
  186. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{
  187. Filters: filters.NewArgs(
  188. networkFilter("default"),
  189. projectFilter(strings.ToLower(testProject)),
  190. ),
  191. }).Return([]network.Summary{{ID: "abc123", Name: "myProject_default"}}, nil)
  192. api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
  193. api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
  194. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
  195. assert.NilError(t, err)
  196. }
  197. func TestDownRemoveVolumes(t *testing.T) {
  198. mockCtrl := gomock.NewController(t)
  199. defer mockCtrl.Finish()
  200. api, cli := prepareMocks(mockCtrl)
  201. tested, err := NewComposeService(cli)
  202. assert.NilError(t, err)
  203. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  204. []container.Summary{testContainer("service1", "123", false)}, nil)
  205. api.EXPECT().VolumeList(
  206. gomock.Any(),
  207. volume.ListOptions{
  208. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  209. }).
  210. Return(volume.ListResponse{
  211. Volumes: []*volume.Volume{{Name: "myProject_volume"}},
  212. }, nil)
  213. api.EXPECT().VolumeInspect(gomock.Any(), "myProject_volume").
  214. Return(volume.Volume{}, nil)
  215. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  216. Return(nil, nil)
  217. api.EXPECT().ContainerStop(gomock.Any(), "123", container.StopOptions{}).Return(nil)
  218. api.EXPECT().ContainerRemove(gomock.Any(), "123", container.RemoveOptions{Force: true, RemoveVolumes: true}).Return(nil)
  219. api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil)
  220. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
  221. assert.NilError(t, err)
  222. }
  223. func TestDownRemoveImages(t *testing.T) {
  224. mockCtrl := gomock.NewController(t)
  225. defer mockCtrl.Finish()
  226. opts := compose.DownOptions{
  227. Project: &types.Project{
  228. Name: strings.ToLower(testProject),
  229. Services: types.Services{
  230. "local-anonymous": {Name: "local-anonymous"},
  231. "local-named": {Name: "local-named", Image: "local-named-image"},
  232. "remote": {Name: "remote", Image: "remote-image"},
  233. "remote-tagged": {Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"},
  234. "no-images-anonymous": {Name: "no-images-anonymous"},
  235. "no-images-named": {Name: "no-images-named", Image: "missing-named-image"},
  236. },
  237. },
  238. }
  239. api, cli := prepareMocks(mockCtrl)
  240. tested, err := NewComposeService(cli)
  241. assert.NilError(t, err)
  242. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).
  243. Return([]container.Summary{
  244. testContainer("service1", "123", false),
  245. }, nil).
  246. AnyTimes()
  247. api.EXPECT().ImageList(gomock.Any(), image.ListOptions{
  248. Filters: filters.NewArgs(
  249. projectFilter(strings.ToLower(testProject)),
  250. filters.Arg("dangling", "false"),
  251. ),
  252. }).Return([]image.Summary{
  253. {
  254. Labels: types.Labels{compose.ServiceLabel: "local-anonymous"},
  255. RepoTags: []string{"testproject-local-anonymous:latest"},
  256. },
  257. {
  258. Labels: types.Labels{compose.ServiceLabel: "local-named"},
  259. RepoTags: []string{"local-named-image:latest"},
  260. },
  261. }, nil).AnyTimes()
  262. imagesToBeInspected := map[string]bool{
  263. "testproject-local-anonymous": true,
  264. "local-named-image": true,
  265. "remote-image": true,
  266. "testproject-no-images-anonymous": false,
  267. "missing-named-image": false,
  268. }
  269. for img, exists := range imagesToBeInspected {
  270. var resp image.InspectResponse
  271. var err error
  272. if exists {
  273. resp.RepoTags = []string{img}
  274. } else {
  275. err = errdefs.ErrNotFound.WithMessage(fmt.Sprintf("test specified that image %q should not exist", img))
  276. }
  277. api.EXPECT().ImageInspect(gomock.Any(), img).
  278. Return(resp, err).
  279. AnyTimes()
  280. }
  281. api.EXPECT().ImageInspect(gomock.Any(), "registry.example.com/remote-image-tagged:v1.0").
  282. Return(image.InspectResponse{RepoTags: []string{"registry.example.com/remote-image-tagged:v1.0"}}, nil).
  283. AnyTimes()
  284. localImagesToBeRemoved := []string{
  285. "testproject-local-anonymous:latest",
  286. "local-named-image:latest",
  287. }
  288. for _, img := range localImagesToBeRemoved {
  289. // test calls down --rmi=local then down --rmi=all, so local images
  290. // get "removed" 2x, while other images are only 1x
  291. api.EXPECT().ImageRemove(gomock.Any(), img, image.RemoveOptions{}).
  292. Return(nil, nil).
  293. Times(2)
  294. }
  295. t.Log("-> docker compose down --rmi=local")
  296. opts.Images = "local"
  297. err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
  298. assert.NilError(t, err)
  299. otherImagesToBeRemoved := []string{
  300. "remote-image:latest",
  301. "registry.example.com/remote-image-tagged:v1.0",
  302. }
  303. for _, img := range otherImagesToBeRemoved {
  304. api.EXPECT().ImageRemove(gomock.Any(), img, image.RemoveOptions{}).
  305. Return(nil, nil).
  306. Times(1)
  307. }
  308. t.Log("-> docker compose down --rmi=all")
  309. opts.Images = "all"
  310. err = tested.Down(t.Context(), strings.ToLower(testProject), opts)
  311. assert.NilError(t, err)
  312. }
  313. func TestDownRemoveImages_NoLabel(t *testing.T) {
  314. mockCtrl := gomock.NewController(t)
  315. defer mockCtrl.Finish()
  316. api, cli := prepareMocks(mockCtrl)
  317. tested, err := NewComposeService(cli)
  318. assert.NilError(t, err)
  319. ctr := testContainer("service1", "123", false)
  320. api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
  321. []container.Summary{ctr}, nil)
  322. api.EXPECT().VolumeList(
  323. gomock.Any(),
  324. volume.ListOptions{
  325. Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
  326. }).
  327. Return(volume.ListResponse{
  328. Volumes: []*volume.Volume{{Name: "myProject_volume"}},
  329. }, nil)
  330. api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
  331. Return(nil, nil)
  332. // ImageList returns no images for the project since they were unlabeled
  333. // (created by an older version of Compose)
  334. api.EXPECT().ImageList(gomock.Any(), image.ListOptions{
  335. Filters: filters.NewArgs(
  336. projectFilter(strings.ToLower(testProject)),
  337. filters.Arg("dangling", "false"),
  338. ),
  339. }).Return(nil, nil)
  340. api.EXPECT().ImageInspect(gomock.Any(), "testproject-service1").
  341. Return(image.InspectResponse{}, nil)
  342. api.EXPECT().ContainerStop(gomock.Any(), "123", container.StopOptions{}).Return(nil)
  343. api.EXPECT().ContainerRemove(gomock.Any(), "123", container.RemoveOptions{Force: true}).Return(nil)
  344. api.EXPECT().ImageRemove(gomock.Any(), "testproject-service1:latest", image.RemoveOptions{}).Return(nil, nil)
  345. err = tested.Down(t.Context(), strings.ToLower(testProject), compose.DownOptions{Images: "local"})
  346. assert.NilError(t, err)
  347. }
  348. func prepareMocks(mockCtrl *gomock.Controller) (*mocks.MockAPIClient, *mocks.MockCli) {
  349. api := mocks.NewMockAPIClient(mockCtrl)
  350. cli := mocks.NewMockCli(mockCtrl)
  351. cli.EXPECT().Client().Return(api).AnyTimes()
  352. cli.EXPECT().Err().Return(streams.NewOut(os.Stderr)).AnyTimes()
  353. cli.EXPECT().Out().Return(streams.NewOut(os.Stdout)).AnyTimes()
  354. return api, cli
  355. }