down.go 11 KB

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