down.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 key, n := range project.Networks {
  148. if n.External.External {
  149. continue
  150. }
  151. // loop capture variable for op closure
  152. networkKey := key
  153. idOrName := n.Name
  154. ops = append(ops, func() error {
  155. return s.removeNetwork(ctx, networkKey, project.Name, idOrName, w)
  156. })
  157. }
  158. return ops
  159. }
  160. func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string, w progress.Writer) error {
  161. networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
  162. Filters: filters.NewArgs(
  163. projectFilter(projectName),
  164. networkFilter(composeNetworkName)),
  165. })
  166. if err != nil {
  167. return errors.Wrapf(err, "failed to list networks")
  168. }
  169. if len(networks) == 0 {
  170. return nil
  171. }
  172. eventName := fmt.Sprintf("Network %s", name)
  173. w.Event(progress.RemovingEvent(eventName))
  174. var found int
  175. for _, net := range networks {
  176. if net.Name != name {
  177. continue
  178. }
  179. network, err := s.apiClient().NetworkInspect(ctx, net.ID, moby.NetworkInspectOptions{})
  180. if errdefs.IsNotFound(err) {
  181. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  182. return nil
  183. }
  184. if err != nil {
  185. return err
  186. }
  187. if len(network.Containers) > 0 {
  188. w.Event(progress.NewEvent(eventName, progress.Warning, "Resource is still in use"))
  189. found++
  190. continue
  191. }
  192. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  193. if errdefs.IsNotFound(err) {
  194. continue
  195. }
  196. w.Event(progress.ErrorEvent(eventName))
  197. return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
  198. }
  199. w.Event(progress.RemovedEvent(eventName))
  200. found++
  201. }
  202. if found == 0 {
  203. // in practice, it's extremely unlikely for this to ever occur, as it'd
  204. // mean the network was present when we queried at the start of this
  205. // method but was then deleted by something else in the interim
  206. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  207. return nil
  208. }
  209. return nil
  210. }
  211. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  212. id := fmt.Sprintf("Image %s", image)
  213. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  214. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  215. if err == nil {
  216. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  217. return nil
  218. }
  219. if errdefs.IsConflict(err) {
  220. w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
  221. return nil
  222. }
  223. if errdefs.IsNotFound(err) {
  224. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  225. return nil
  226. }
  227. return err
  228. }
  229. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  230. resource := fmt.Sprintf("Volume %s", id)
  231. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  232. err := s.apiClient().VolumeRemove(ctx, id, true)
  233. if err == nil {
  234. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  235. return nil
  236. }
  237. if errdefs.IsConflict(err) {
  238. w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
  239. return nil
  240. }
  241. if errdefs.IsNotFound(err) {
  242. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  243. return nil
  244. }
  245. return err
  246. }
  247. func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
  248. eventName := getContainerProgressName(container)
  249. w.Event(progress.StoppingEvent(eventName))
  250. timeoutInSecond := utils.DurationSecondToInt(timeout)
  251. err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
  252. if err != nil {
  253. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  254. return err
  255. }
  256. w.Event(progress.StoppedEvent(eventName))
  257. return nil
  258. }
  259. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  260. eg, ctx := errgroup.WithContext(ctx)
  261. for _, container := range containers {
  262. container := container
  263. eg.Go(func() error {
  264. return s.stopContainer(ctx, w, container, timeout)
  265. })
  266. }
  267. return eg.Wait()
  268. }
  269. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  270. eg, _ := errgroup.WithContext(ctx)
  271. for _, container := range containers {
  272. container := container
  273. eg.Go(func() error {
  274. eventName := getContainerProgressName(container)
  275. err := s.stopContainer(ctx, w, container, timeout)
  276. if err != nil {
  277. return err
  278. }
  279. w.Event(progress.RemovingEvent(eventName))
  280. err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
  281. Force: true,
  282. RemoveVolumes: volumes,
  283. })
  284. if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
  285. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  286. return err
  287. }
  288. w.Event(progress.RemovedEvent(eventName))
  289. return nil
  290. })
  291. }
  292. return eg.Wait()
  293. }
  294. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  295. containers = containers.filter(isNotOneOff)
  296. project, err := s.projectFromName(containers, projectName)
  297. if err != nil && !api.IsNotFoundError(err) {
  298. return nil, err
  299. }
  300. volumes, err := s.actualVolumes(ctx, projectName)
  301. if err != nil {
  302. return nil, err
  303. }
  304. project.Volumes = volumes
  305. networks, err := s.actualNetworks(ctx, projectName)
  306. if err != nil {
  307. return nil, err
  308. }
  309. project.Networks = networks
  310. return project, nil
  311. }