down.go 11 KB

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