down.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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.getServiceImagesToRemove(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) getServiceImagesToRemove(options api.DownOptions, project *types.Project) map[string]struct{} {
  165. images := map[string]struct{}{}
  166. for _, service := range project.Services {
  167. image, ok := service.Labels[api.ImageNameLabel] // Information on the compose file at the creation of the container
  168. if !ok || (options.Images == "local" && image != "") {
  169. continue
  170. }
  171. image = api.GetImageNameOrDefault(service, project.Name)
  172. images[image] = struct{}{}
  173. }
  174. return images
  175. }
  176. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  177. id := fmt.Sprintf("Image %s", image)
  178. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  179. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  180. if err == nil {
  181. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  182. return nil
  183. }
  184. if errdefs.IsNotFound(err) {
  185. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  186. return nil
  187. }
  188. return err
  189. }
  190. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  191. resource := fmt.Sprintf("Volume %s", id)
  192. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  193. err := s.apiClient().VolumeRemove(ctx, id, true)
  194. if err == nil {
  195. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  196. return nil
  197. }
  198. if errdefs.IsNotFound(err) {
  199. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  200. return nil
  201. }
  202. return err
  203. }
  204. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  205. eg, ctx := errgroup.WithContext(ctx)
  206. for _, container := range containers {
  207. container := container
  208. eg.Go(func() error {
  209. eventName := getContainerProgressName(container)
  210. w.Event(progress.StoppingEvent(eventName))
  211. err := s.apiClient().ContainerStop(ctx, container.ID, timeout)
  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. }
  220. return eg.Wait()
  221. }
  222. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  223. eg, _ := errgroup.WithContext(ctx)
  224. for _, container := range containers {
  225. container := container
  226. eg.Go(func() error {
  227. eventName := getContainerProgressName(container)
  228. w.Event(progress.StoppingEvent(eventName))
  229. err := s.stopContainers(ctx, w, []moby.Container{container}, timeout)
  230. if err != nil {
  231. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  232. return err
  233. }
  234. w.Event(progress.RemovingEvent(eventName))
  235. err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
  236. Force: true,
  237. RemoveVolumes: volumes,
  238. })
  239. if err != nil {
  240. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  241. return err
  242. }
  243. w.Event(progress.RemovedEvent(eventName))
  244. return nil
  245. })
  246. }
  247. return eg.Wait()
  248. }
  249. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  250. containers = containers.filter(isNotOneOff)
  251. project, err := s.projectFromName(containers, projectName)
  252. if err != nil && !api.IsNotFoundError(err) {
  253. return nil, err
  254. }
  255. volumes, err := s.actualVolumes(ctx, projectName)
  256. if err != nil {
  257. return nil, err
  258. }
  259. project.Volumes = volumes
  260. networks, err := s.actualNetworks(ctx, projectName)
  261. if err != nil {
  262. return nil, err
  263. }
  264. project.Networks = networks
  265. return project, nil
  266. }