down.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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. "context"
  16. "fmt"
  17. "strings"
  18. "time"
  19. "github.com/docker/compose/v2/pkg/utils"
  20. "github.com/compose-spec/compose-go/types"
  21. moby "github.com/docker/docker/api/types"
  22. containerType "github.com/docker/docker/api/types/container"
  23. "github.com/docker/docker/api/types/filters"
  24. "github.com/docker/docker/errdefs"
  25. "github.com/pkg/errors"
  26. "golang.org/x/sync/errgroup"
  27. "github.com/docker/compose/v2/pkg/api"
  28. "github.com/docker/compose/v2/pkg/progress"
  29. )
  30. type downOp func() error
  31. func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
  32. return progress.Run(ctx, func(ctx context.Context) error {
  33. return s.down(ctx, strings.ToLower(projectName), options)
  34. }, s.stderr())
  35. }
  36. func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
  37. w := progress.ContextWriter(ctx)
  38. resourceToRemove := false
  39. include := oneOffExclude
  40. if options.RemoveOrphans {
  41. include = oneOffInclude
  42. }
  43. containers, err := s.getContainers(ctx, projectName, include, true)
  44. if err != nil {
  45. return err
  46. }
  47. project := options.Project
  48. if project == nil {
  49. project, err = s.getProjectWithResources(ctx, containers, projectName)
  50. if err != nil {
  51. return err
  52. }
  53. }
  54. if len(containers) > 0 {
  55. resourceToRemove = true
  56. }
  57. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
  58. serviceContainers := containers.filter(isService(service))
  59. err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
  60. return err
  61. })
  62. if err != nil {
  63. return err
  64. }
  65. orphans := containers.filter(isNotService(project.ServiceNames()...))
  66. if options.RemoveOrphans && len(orphans) > 0 {
  67. err := s.removeContainers(ctx, w, orphans, options.Timeout, false)
  68. if err != nil {
  69. return err
  70. }
  71. }
  72. ops := s.ensureNetworksDown(ctx, project, w)
  73. if options.Images != "" {
  74. imgOps, err := s.ensureImagesDown(ctx, project, options, w)
  75. if err != nil {
  76. return err
  77. }
  78. ops = append(ops, imgOps...)
  79. }
  80. if options.Volumes {
  81. ops = append(ops, s.ensureVolumesDown(ctx, project, w)...)
  82. }
  83. if !resourceToRemove && len(ops) == 0 {
  84. fmt.Fprintf(s.stderr(), "Warning: No resource found to remove for project %q.\n", projectName)
  85. }
  86. eg, _ := errgroup.WithContext(ctx)
  87. for _, op := range ops {
  88. eg.Go(op)
  89. }
  90. return eg.Wait()
  91. }
  92. func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  93. var ops []downOp
  94. for _, vol := range project.Volumes {
  95. if vol.External.External {
  96. continue
  97. }
  98. volumeName := vol.Name
  99. ops = append(ops, func() error {
  100. return s.removeVolume(ctx, volumeName, w)
  101. })
  102. }
  103. return ops
  104. }
  105. func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) ([]downOp, error) {
  106. imagePruner := NewImagePruner(s.apiClient(), project)
  107. pruneOpts := ImagePruneOptions{
  108. Mode: ImagePruneMode(options.Images),
  109. RemoveOrphans: options.RemoveOrphans,
  110. }
  111. images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
  112. if err != nil {
  113. return nil, err
  114. }
  115. var ops []downOp
  116. for i := range images {
  117. img := images[i]
  118. ops = append(ops, func() error {
  119. return s.removeImage(ctx, img, w)
  120. })
  121. }
  122. return ops, nil
  123. }
  124. func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  125. var ops []downOp
  126. for _, n := range project.Networks {
  127. if n.External.External {
  128. continue
  129. }
  130. // loop capture variable for op closure
  131. networkName := n.Name
  132. ops = append(ops, func() error {
  133. return s.removeNetwork(ctx, networkName, w)
  134. })
  135. }
  136. return ops
  137. }
  138. func (s *composeService) removeNetwork(ctx context.Context, name string, w progress.Writer) error {
  139. // networks are guaranteed to have unique IDs but NOT names, so it's
  140. // possible to get into a situation where a compose down will fail with
  141. // an error along the lines of:
  142. // failed to remove network test: Error response from daemon: network test is ambiguous (2 matches found based on name)
  143. // as a workaround here, the delete is done by ID after doing a list using
  144. // the name as a filter (99.9% of the time this will return a single result)
  145. networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
  146. Filters: filters.NewArgs(filters.Arg("name", name)),
  147. })
  148. if err != nil {
  149. return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
  150. }
  151. if len(networks) == 0 {
  152. return nil
  153. }
  154. eventName := fmt.Sprintf("Network %s", name)
  155. w.Event(progress.RemovingEvent(eventName))
  156. var removed int
  157. for _, net := range networks {
  158. if net.Name == name {
  159. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  160. if errdefs.IsNotFound(err) {
  161. continue
  162. }
  163. w.Event(progress.ErrorEvent(eventName))
  164. return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
  165. }
  166. removed++
  167. }
  168. }
  169. if removed == 0 {
  170. // in practice, it's extremely unlikely for this to ever occur, as it'd
  171. // mean the network was present when we queried at the start of this
  172. // method but was then deleted by something else in the interim
  173. w.Event(progress.NewEvent(eventName, progress.Done, "Warning: No resource found to remove"))
  174. return nil
  175. }
  176. w.Event(progress.RemovedEvent(eventName))
  177. return nil
  178. }
  179. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  180. id := fmt.Sprintf("Image %s", image)
  181. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  182. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  183. if err == nil {
  184. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  185. return nil
  186. }
  187. if errdefs.IsNotFound(err) {
  188. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  189. return nil
  190. }
  191. return err
  192. }
  193. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  194. resource := fmt.Sprintf("Volume %s", id)
  195. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  196. err := s.apiClient().VolumeRemove(ctx, id, true)
  197. if err == nil {
  198. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  199. return nil
  200. }
  201. if errdefs.IsNotFound(err) {
  202. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  203. return nil
  204. }
  205. return err
  206. }
  207. func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
  208. eventName := getContainerProgressName(container)
  209. w.Event(progress.StoppingEvent(eventName))
  210. timeoutInSecond := utils.DurationSecondToInt(timeout)
  211. err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
  212. if err != nil {
  213. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  214. return err
  215. }
  216. w.Event(progress.StoppedEvent(eventName))
  217. return nil
  218. }
  219. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  220. eg, ctx := errgroup.WithContext(ctx)
  221. for _, container := range containers {
  222. container := container
  223. eg.Go(func() error {
  224. return s.stopContainer(ctx, w, container, timeout)
  225. })
  226. }
  227. return eg.Wait()
  228. }
  229. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  230. eg, _ := errgroup.WithContext(ctx)
  231. for _, container := range containers {
  232. container := container
  233. eg.Go(func() error {
  234. eventName := getContainerProgressName(container)
  235. err := s.stopContainer(ctx, w, container, timeout)
  236. if err != nil {
  237. return err
  238. }
  239. w.Event(progress.RemovingEvent(eventName))
  240. err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
  241. Force: true,
  242. RemoveVolumes: volumes,
  243. })
  244. if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
  245. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  246. return err
  247. }
  248. w.Event(progress.RemovedEvent(eventName))
  249. return nil
  250. })
  251. }
  252. return eg.Wait()
  253. }
  254. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  255. containers = containers.filter(isNotOneOff)
  256. project, err := s.projectFromName(containers, projectName)
  257. if err != nil && !api.IsNotFoundError(err) {
  258. return nil, err
  259. }
  260. volumes, err := s.actualVolumes(ctx, projectName)
  261. if err != nil {
  262. return nil, err
  263. }
  264. project.Volumes = volumes
  265. networks, err := s.actualNetworks(ctx, projectName)
  266. if err != nil {
  267. return nil, err
  268. }
  269. project.Networks = networks
  270. return project, nil
  271. }