down.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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/v2/types"
  20. "github.com/docker/compose/v2/internal/desktop"
  21. "github.com/docker/compose/v2/pkg/utils"
  22. moby "github.com/docker/docker/api/types"
  23. containerType "github.com/docker/docker/api/types/container"
  24. "github.com/docker/docker/api/types/filters"
  25. "github.com/docker/docker/errdefs"
  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, serviceContainers, options.Timeout, options.Volumes)
  65. return err
  66. }, WithRootNodesAndDown(options.Services))
  67. if err != nil {
  68. return err
  69. }
  70. orphans := containers.filter(isOrphaned(project))
  71. if options.RemoveOrphans && len(orphans) > 0 {
  72. err := s.removeContainers(ctx, 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 {
  117. continue
  118. }
  119. volumeName := vol.Name
  120. ops = append(ops, func() error {
  121. return s.removeVolume(ctx, volumeName, w)
  122. })
  123. }
  124. if s.experiments.AutoFileShares() && s.desktopCli != nil {
  125. ops = append(ops, func() error {
  126. desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
  127. return nil
  128. })
  129. }
  130. return ops
  131. }
  132. func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) ([]downOp, error) {
  133. imagePruner := NewImagePruner(s.apiClient(), project)
  134. pruneOpts := ImagePruneOptions{
  135. Mode: ImagePruneMode(options.Images),
  136. RemoveOrphans: options.RemoveOrphans,
  137. }
  138. images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
  139. if err != nil {
  140. return nil, err
  141. }
  142. var ops []downOp
  143. for i := range images {
  144. img := images[i]
  145. ops = append(ops, func() error {
  146. return s.removeImage(ctx, img, w)
  147. })
  148. }
  149. return ops, nil
  150. }
  151. func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  152. var ops []downOp
  153. for key, n := range project.Networks {
  154. if n.External {
  155. continue
  156. }
  157. // loop capture variable for op closure
  158. networkKey := key
  159. idOrName := n.Name
  160. ops = append(ops, func() error {
  161. return s.removeNetwork(ctx, networkKey, project.Name, idOrName, w)
  162. })
  163. }
  164. return ops
  165. }
  166. func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string, w progress.Writer) error {
  167. networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
  168. Filters: filters.NewArgs(
  169. projectFilter(projectName),
  170. networkFilter(composeNetworkName)),
  171. })
  172. if err != nil {
  173. return fmt.Errorf("failed to list networks: %w", err)
  174. }
  175. if len(networks) == 0 {
  176. return nil
  177. }
  178. eventName := fmt.Sprintf("Network %s", name)
  179. w.Event(progress.RemovingEvent(eventName))
  180. var found int
  181. for _, net := range networks {
  182. if net.Name != name {
  183. continue
  184. }
  185. network, err := s.apiClient().NetworkInspect(ctx, net.ID, moby.NetworkInspectOptions{})
  186. if errdefs.IsNotFound(err) {
  187. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  188. return nil
  189. }
  190. if err != nil {
  191. return err
  192. }
  193. if len(network.Containers) > 0 {
  194. w.Event(progress.NewEvent(eventName, progress.Warning, "Resource is still in use"))
  195. found++
  196. continue
  197. }
  198. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  199. if errdefs.IsNotFound(err) {
  200. continue
  201. }
  202. w.Event(progress.ErrorEvent(eventName))
  203. return fmt.Errorf("failed to remove network %s: %w", name, err)
  204. }
  205. w.Event(progress.RemovedEvent(eventName))
  206. found++
  207. }
  208. if found == 0 {
  209. // in practice, it's extremely unlikely for this to ever occur, as it'd
  210. // mean the network was present when we queried at the start of this
  211. // method but was then deleted by something else in the interim
  212. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  213. return nil
  214. }
  215. return nil
  216. }
  217. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  218. id := fmt.Sprintf("Image %s", image)
  219. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  220. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  221. if err == nil {
  222. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  223. return nil
  224. }
  225. if errdefs.IsConflict(err) {
  226. w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
  227. return nil
  228. }
  229. if errdefs.IsNotFound(err) {
  230. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  231. return nil
  232. }
  233. return err
  234. }
  235. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  236. resource := fmt.Sprintf("Volume %s", id)
  237. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  238. err := s.apiClient().VolumeRemove(ctx, id, true)
  239. if err == nil {
  240. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  241. return nil
  242. }
  243. if errdefs.IsConflict(err) {
  244. w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
  245. return nil
  246. }
  247. if errdefs.IsNotFound(err) {
  248. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  249. return nil
  250. }
  251. return err
  252. }
  253. func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
  254. eventName := getContainerProgressName(container)
  255. w.Event(progress.StoppingEvent(eventName))
  256. timeoutInSecond := utils.DurationSecondToInt(timeout)
  257. err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
  258. if err != nil {
  259. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  260. return err
  261. }
  262. w.Event(progress.StoppedEvent(eventName))
  263. return nil
  264. }
  265. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  266. eg, ctx := errgroup.WithContext(ctx)
  267. for _, container := range containers {
  268. container := container
  269. eg.Go(func() error {
  270. return s.stopContainer(ctx, w, container, timeout)
  271. })
  272. }
  273. return eg.Wait()
  274. }
  275. func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  276. eg, _ := errgroup.WithContext(ctx)
  277. for _, container := range containers {
  278. container := container
  279. eg.Go(func() error {
  280. return s.stopAndRemoveContainer(ctx, container, timeout, volumes)
  281. })
  282. }
  283. return eg.Wait()
  284. }
  285. func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, timeout *time.Duration, volumes bool) error {
  286. w := progress.ContextWriter(ctx)
  287. eventName := getContainerProgressName(container)
  288. err := s.stopContainer(ctx, w, container, timeout)
  289. if err != nil {
  290. return err
  291. }
  292. w.Event(progress.RemovingEvent(eventName))
  293. err = s.apiClient().ContainerRemove(ctx, container.ID, containerType.RemoveOptions{
  294. Force: true,
  295. RemoveVolumes: volumes,
  296. })
  297. if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
  298. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  299. return err
  300. }
  301. w.Event(progress.RemovedEvent(eventName))
  302. return nil
  303. }
  304. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  305. containers = containers.filter(isNotOneOff)
  306. project, err := s.projectFromName(containers, projectName)
  307. if err != nil && !api.IsNotFoundError(err) {
  308. return nil, err
  309. }
  310. volumes, err := s.actualVolumes(ctx, projectName)
  311. if err != nil {
  312. return nil, err
  313. }
  314. project.Volumes = volumes
  315. networks, err := s.actualNetworks(ctx, projectName)
  316. if err != nil {
  317. return nil, err
  318. }
  319. project.Networks = networks
  320. return project, nil
  321. }