down.go 10 KB

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