down.go 10 KB

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