down.go 8.8 KB

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