down.go 8.9 KB

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