down.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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 net.Name == name {
  141. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  142. if errdefs.IsNotFound(err) {
  143. continue
  144. }
  145. w.Event(progress.ErrorEvent(eventName))
  146. return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
  147. }
  148. removed++
  149. }
  150. }
  151. if removed == 0 {
  152. // in practice, it's extremely unlikely for this to ever occur, as it'd
  153. // mean the network was present when we queried at the start of this
  154. // method but was then deleted by something else in the interim
  155. w.Event(progress.NewEvent(eventName, progress.Done, "Warning: No resource found to remove"))
  156. return nil
  157. }
  158. w.Event(progress.RemovedEvent(eventName))
  159. return nil
  160. }
  161. func (s *composeService) getServiceImages(options api.DownOptions, project *types.Project) map[string]struct{} {
  162. images := map[string]struct{}{}
  163. for _, service := range project.Services {
  164. image := service.Image
  165. if options.Images == "local" && image != "" {
  166. continue
  167. }
  168. if image == "" {
  169. image = getImageName(service, project.Name)
  170. }
  171. images[image] = struct{}{}
  172. }
  173. return images
  174. }
  175. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  176. id := fmt.Sprintf("Image %s", image)
  177. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  178. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  179. if err == nil {
  180. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  181. return nil
  182. }
  183. if errdefs.IsNotFound(err) {
  184. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  185. return nil
  186. }
  187. return err
  188. }
  189. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  190. resource := fmt.Sprintf("Volume %s", id)
  191. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  192. err := s.apiClient().VolumeRemove(ctx, id, true)
  193. if err == nil {
  194. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  195. return nil
  196. }
  197. if errdefs.IsNotFound(err) {
  198. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  199. return nil
  200. }
  201. return err
  202. }
  203. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  204. eg, ctx := errgroup.WithContext(ctx)
  205. for _, container := range containers {
  206. container := container
  207. eg.Go(func() error {
  208. eventName := getContainerProgressName(container)
  209. w.Event(progress.StoppingEvent(eventName))
  210. err := s.apiClient().ContainerStop(ctx, container.ID, timeout)
  211. if err != nil {
  212. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  213. return err
  214. }
  215. w.Event(progress.StoppedEvent(eventName))
  216. return nil
  217. })
  218. }
  219. return eg.Wait()
  220. }
  221. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  222. eg, _ := errgroup.WithContext(ctx)
  223. for _, container := range containers {
  224. container := container
  225. eg.Go(func() error {
  226. eventName := getContainerProgressName(container)
  227. w.Event(progress.StoppingEvent(eventName))
  228. err := s.stopContainers(ctx, w, []moby.Container{container}, timeout)
  229. if err != nil {
  230. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  231. return err
  232. }
  233. w.Event(progress.RemovingEvent(eventName))
  234. err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
  235. Force: true,
  236. RemoveVolumes: volumes,
  237. })
  238. if err != nil {
  239. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  240. return err
  241. }
  242. w.Event(progress.RemovedEvent(eventName))
  243. return nil
  244. })
  245. }
  246. return eg.Wait()
  247. }
  248. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  249. containers = containers.filter(isNotOneOff)
  250. project, err := s.projectFromName(containers, projectName)
  251. if err != nil && !api.IsNotFoundError(err) {
  252. return nil, err
  253. }
  254. volumes, err := s.actualVolumes(ctx, projectName)
  255. if err != nil {
  256. return nil, err
  257. }
  258. project.Volumes = volumes
  259. networks, err := s.actualNetworks(ctx, projectName)
  260. if err != nil {
  261. return nil, err
  262. }
  263. project.Networks = networks
  264. return project, nil
  265. }