down.go 8.8 KB

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